# _*_ coding: utf-8 _*_

import numpy as np
from random import randint
from random import uniform
from copy import deepcopy
import matplotlib.pyplot as plt

from scripts.utils.operators import Lifecycle
from scripts.utils.operators import lhs_uniform

np.set_printoptions(linewidth=999, threshold=round(1e9))


class ABC:
    """
    Artificial Bee Colony (ABC)
    colony consists of employed bees,
    the bees can change their roles in different situations:
    exploitation : employed bees -> onlooker bees
    stagnation : employed bees -> scout bees
    """

    def __init__(self, obj_func, dimension: int, bounds: tuple, NP: int, SL: int):
        self.obj_func, self.__d, self.__bounds = obj_func, dimension, bounds
        self.__NP, self.__SL = NP, SL

        lhs = lhs_uniform(n=self.__NP, p=self.__d)
        x = (self.__bounds[0] + lhs * (self.__bounds[1] - self.__bounds[0]))  # position
        # colony[i] structure: [x, fitness, stagnation]
        self.__colony = [[x[i], 0, 0] for i in range(self.__NP)]
        for i in range(self.__NP):
            self.__colony[i][1] = self.obj_func(self.__colony[i][0])
        self.gBest = deepcopy(min(self.__colony, key=lambda _: _[1]))

        self.life = Lifecycle()

    def optimize(self, iterations=None, evaluations=None, deadline=None):
        self.life.set_stopping_condition(iterations=iterations, evaluations=evaluations, deadline=deadline)
        self.life.update(eva=self.__NP)
        while self.life.check_stopping_condition() is False:
            self.__employed()
            self.__onlooker()
            if self.gBest[1] > min(self.__colony, key=lambda _: _[1])[1]:
                self.gBest = deepcopy(min(self.__colony, key=lambda _: _[1]))
            self.__scout()

            self.life.update(it=1, gBest=self.gBest[1])

            # plt.cla()
            # plt.plot(range(len(self.history.gBest)), self.history.gBest)
            # plt.pause(0.001)

        return self.life

    def __forage(self, index: int):
        k = randint(0, self.__NP - 1)
        if k == index:
            k = randint(0, self.__NP - 1)  # select another solution
        x = np.zeros(self.__d)  # new solution
        for i in range(len(self.__colony[index][0])):
            x[i] = self.__colony[index][0][i] + uniform(-1, 1) * (self.__colony[index][0][i] - self.__colony[k][0][i])
            if x[i] < self.__bounds[0]:
                x[i] = self.__bounds[0]
            if x[i] > self.__bounds[1]:
                x[i] = self.__bounds[1]
        fitness = self.obj_func(x)
        return [x, fitness, self.__colony[index][2]]

    def __employed(self):
        for i, bee in enumerate(self.__colony):
            new_food = self.__forage(index=i)
            if new_food[1] < bee[1]:
                self.__colony[i] = deepcopy(new_food)
                self.__colony[i][2] = 0
            else:
                self.__colony[i][2] += 1

    def __onlooker(self):
        for i, bee in enumerate(self.__colony):
            a = randint(0, self.__NP - 1)
            b = randint(0, self.__NP - 1)
            if b == a:
                b = randint(0, self.__NP - 1)
            c = a if self.__colony[a][1] < self.__colony[b][1] else b
            new_food = self.__forage(index=c)
            if new_food[1] < bee[1]:
                self.__colony[i] = deepcopy(new_food)
                self.__colony[i][2] = 0
            else:
                self.__colony[i][2] += 1

    def __scout(self):
        for i, bee in enumerate(self.__colony):
            if bee[2] >= self.__SL:
                self.__colony[i][0] = np.random.uniform(low=self.__bounds[0], high=self.__bounds[1],
                                                        size=self.__d)
                self.__colony[i][1] = self.obj_func(self.__colony[i][0])
                self.__colony[i][2] = 0
